/*
 * *****************************************************************************************************************
 * Copyright 2012 Michael Pellaton
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
 * the specific language governing permissions and limitations under the License.
 *
 * Contributors:
 *   Michael Pellaton
 * *****************************************************************************************************************
 */
package ch.contrails.springconfigvalidation;

import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

/**
 * <pre>
 * Class Checks
 * [x] Error: Invalid bean definition class: @Configuration classes must not be final.
 * [x] Error: Invalid bean definition class: @Configuration classes must have a visible no-arg constructor.
 * [x] Error: Invalid bean definition class: @Configuration class constructors must not be @Autowired.
 * [x] Error: Invalid bean definition class: Nested @Configuration classes must be static.
 * [ ] Error: Invalid bean definition class: @Configuration classes must not be local.
 * 
 * Method Checks
 * [x] Error: Invalid factory method: @Bean methods must not be private.
 * [x] Error: Invalid factory method: @Bean methods must not be final.
 * [x] Error: Invalid factory method: @Bean methods must have a non-void return type.
 * [x] Warn:  @Bean methods returning a BeanFactoryPostProcessor should be static.
 * [x] Warn:  Only @Bean methods returning a BeanFactoryPostProcessor should be static.
 * [x] Warn:  Non-private non-abstract methods in bean definition classes should be @Bean methods. 
 * </pre>
 * 
 * @author Michael Pellaton
 * 
 * @see <a href="http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/beans.html#beans-java">Spring Frameowrk Reference: Java-based container configuration</a>
 * @see <a href="http://static.springsource.org/spring/docs/3.1.x/javadoc-api/org/springframework/context/annotation/Configuration.html">Spring API Documentation: Configuration</a>
 * @see <a href="http://static.springsource.org/spring/docs/3.1.x/javadoc-api/org/springframework/context/annotation/Bean.html">Spring API Documentation: Bean</a> 
 */

@SupportedAnnotationTypes(value = { "org.springframework.context.annotation.Configuration" })
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class SpringConfigurationValidationProcessor extends AbstractProcessor {

  private TypeElement configurationTypeElement;
  private TypeElement beanTypeElement;
  private TypeElement bfppTypeElement;

  private Messager messager;
  private Types typeUtils;
  private TypeElement autowiredTypeElement;

  /**
   * No-argument constructor.
   */
  public SpringConfigurationValidationProcessor() {
    super();
  }

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);

    Elements elementUtils = processingEnv.getElementUtils();
    configurationTypeElement = elementUtils.getTypeElement("org.springframework.context.annotation.Configuration");
    beanTypeElement = elementUtils.getTypeElement("org.springframework.context.annotation.Bean");
    bfppTypeElement = elementUtils.getTypeElement("org.springframework.beans.factory.config.BeanFactoryPostProcessor");
    autowiredTypeElement = elementUtils.getTypeElement("org.springframework.beans.factory.annotation.Autowired");

    messager = processingEnv.getMessager();
    typeUtils = processingEnv.getTypeUtils();
  }

  
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    if (!roundEnv.errorRaised() && !roundEnv.processingOver()) {
      processRound(annotations, roundEnv);
    }
    return false;
  }

  
  private void processRound(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for (TypeElement annotation : annotations) {
      for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
        processElement(element);
      }
    }
  }

  private void processElement(Element element) {
    for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
      Element annotationTypeElement = annotation.getAnnotationType().asElement();
      if (annotationTypeElement.equals(configurationTypeElement)) {
        List<? extends Element> enclosedElements = element.getEnclosedElements();
        
        processClass(element, ElementFilter.constructorsIn(enclosedElements));
        
        for (ExecutableElement method : ElementFilter.methodsIn(enclosedElements)) {
          processMethod(method);
        }
      }
    }
  }

  private void processClass(Element element, List<ExecutableElement> constructors) {
    processClassVisibility(element);
    processConstructors(element, constructors);
    processStaticClass(element);
  }

  private void processStaticClass(Element element) {
    if (isNestedClass(element) && !isStaticMethod(element)) {
      printMessage(SpringConfigurationMessage.NESTED_CLASS_NOT_STATIC, element);
      
    }
    
  }



  private boolean isNestedClass(Element element) {
    return element.getEnclosingElement().getKind().equals(ElementKind.CLASS);
  }


  private void processClassVisibility(Element element) {
    if (element.getModifiers().contains(Modifier.FINAL)) {
      printMessage(SpringConfigurationMessage.CLASS_FINAL, element);
    }
  }

  private void processConstructors(Element element, List<ExecutableElement> constructors) {
    boolean hasVisibleNoArgConsctructor = false;
    for (ExecutableElement constructor : constructors) {
      if (isVisibleElement(constructor) && hasNoParameter(constructor)) {
        hasVisibleNoArgConsctructor = true;
      }
      processAutowiredConstructor(constructor);
    }
    if (!hasVisibleNoArgConsctructor) {
      printMessage(SpringConfigurationMessage.MISSING_NO_ARG_CONSTRUCTOR, element);
    }
  }

  private void processAutowiredConstructor(ExecutableElement constructor) {
    List<? extends AnnotationMirror> annotations = constructor.getAnnotationMirrors();
    for (AnnotationMirror annotationMirror : annotations) {
      
      Element annotationTypeElement = annotationMirror.getAnnotationType().asElement();
      if (annotationTypeElement.equals(autowiredTypeElement) ) {
        printMessage(SpringConfigurationMessage.AUTOWIRED_CONSTRUCTOR, constructor, annotationMirror);
      }
    }
  }

  private boolean hasNoParameter(ExecutableElement constructor) {
    return constructor.getParameters().isEmpty();
  }

  private boolean isVisibleElement(ExecutableElement constructor) {
    return !constructor.getModifiers().contains(Modifier.PRIVATE);
  }

  private void processMethod(ExecutableElement methodElement) {
    List<? extends AnnotationMirror> annotationMirrors = methodElement.getAnnotationMirrors();
    
    if (annotationMirrors.isEmpty()) {
      processForMissingAnnotation(methodElement);
    } else {
      for(AnnotationMirror methodAnnotation : annotationMirrors) {
        Element annotationTypeElement = methodAnnotation.getAnnotationType().asElement();
        if (annotationTypeElement.equals(beanTypeElement) ) {
          processForScope(methodElement);
          processForFinal(methodElement);
          processForReturnType(methodElement);  
          processForBFPP(methodElement);
        } else {
          processForMissingAnnotation(methodElement);
        }
      }
    }
  }

  private void processForFinal(ExecutableElement methodElement) {
    if (isFinalMethod(methodElement)) {
      printMessage(SpringConfigurationMessage.BEAN_METHOD_FINAL, methodElement);
    }
  }

  private void processForMissingAnnotation(ExecutableElement methodElement) {
    if (!isPrivateMethod(methodElement) && !isAbstractMethod(methodElement)) {
      printMessage(SpringConfigurationMessage.POTENTIAL_BEAN_METHOD, methodElement);
    }
  }

  private void processForReturnType(ExecutableElement methodElement) {
    if (methodElement.getReturnType().getKind() == TypeKind.VOID) {
      printMessage(SpringConfigurationMessage.BEAN_METHOD_RETURNS_VOID, methodElement);
    }
  }

  private void processForBFPP(ExecutableElement methodElement) {
    boolean implementsBFPP = typeUtils.isAssignable(methodElement.getReturnType(), bfppTypeElement.asType());
    boolean isStaticMethod = isStaticMethod(methodElement);
    if (isStaticMethod) {
      if (!implementsBFPP) {
        printMessage(SpringConfigurationMessage.STATIC_BEAN_METHOD, methodElement);
      }
    } else {
      if (implementsBFPP) {
        printMessage(SpringConfigurationMessage.BFPP_BEAN_METHOD_NOT_STATIC, methodElement);
      }
    }
  }

  private void processForScope(Element methodElement) {
    if (isPrivateMethod(methodElement)) {
      printMessage(SpringConfigurationMessage.BEAN_METHOD_PRIVATE, methodElement);
    }
  }

  private boolean isPrivateMethod(Element methodElement) {
    return methodElement.getModifiers().contains(Modifier.PRIVATE);
  }

  private boolean isFinalMethod(Element methodElement) {
    return methodElement.getModifiers().contains(Modifier.FINAL);
  }

  private boolean isAbstractMethod(Element methodElement) {
    return methodElement.getModifiers().contains(Modifier.ABSTRACT);
  }

  private boolean isStaticMethod(Element methodElement) {
    return methodElement.getModifiers().contains(Modifier.STATIC);
  }

  
  private void printMessage(SpringConfigurationMessage message, Element element, AnnotationMirror annotationMirror) {
    messager.printMessage(message.getKind(), message.getMessage(), element, annotationMirror);
  }
  
  private void printMessage(SpringConfigurationMessage message, Element element) {
    printMessage(message, element, null);
  }
}
